﻿//----------------------------------------------------------------
// <copyright file="WorkItemRollup.cs" company="Microsoft">
//     Copyright (c) Microsoft Corporation.  All rights reserved.
// </copyright>
//----------------------------------------------------------------

using System;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using System.Text;
using Microsoft.TeamFoundation.Client;
using Microsoft.TeamFoundation.WorkItemTracking.Client;
using MptCodeLibrary.Configuration;
using MptCodeLibrary.Exceptions;

namespace MptCodeLibrary
{
 
    public class WorkItemRollup
    {
        private readonly IMptConfiguration  mptConfiguration;

        private class WorkItemRollupSummary
        {
            public string       TeamProjectName { get; private set; }

            public string       WITCategory     { get; private set; }
            
            public int          WorkItemId      { get; private set; }
            public string       Title           { get; private set; }

            public string       ParentShortName { get; private set; }
            public int          ParentID        { get; private set; } // -1 if unable to parse the value

            public DateTime     StartDate       { get; private set; }
            public DateTime     FinishDate      { get; private set; }

            public double       CompletedWork   { get; private set; }
            public double       RemainingWork   { get; private set; }


            public WorkItemRollupSummary(   string      pTeamProjectName,
                                            string      pWITCategory,
                                            int         pWorkItemId,
                                            string      pTitle,   
                                            string      pParentShortName,
                                            DateTime    pStartDate,
                                            DateTime    pFinishDate,
                                            double      pCompletedWork,
                                            double      pRemainingwork
                )
            {
                TeamProjectName = pTeamProjectName;

                WITCategory     = pWITCategory;

                WorkItemId      = pWorkItemId;
                Title           = pTitle;

                ParentShortName = pParentShortName;


                // ParentID -- calculated by parsing the ID portion of the ParentShortName
                    // Assumes ParentShortName has the following format:  "ID: title value"
                    // For example "123: Some title value"

                        // Default Parent Id if we are unable to parse.
                            ParentID = -1;


                        int IndexOfParentShortNameIDDelimiter = ParentShortName.IndexOf(':');
                        if (IndexOfParentShortNameIDDelimiter != -1)
                        {
                            string  ParentIDSubString = ParentShortName.Substring(0, IndexOfParentShortNameIDDelimiter);
                            int     ParsedParentID;
                            bool    ParseSucceeded = int.TryParse(ParentIDSubString, out ParsedParentID);

                            if (ParseSucceeded)
                                ParentID = ParsedParentID;

                            else
                            {
                                // go figure -- delimiter exists but for some reason we still can't parse the value.
                                // We don't treat failure to find a proper ParentID as a fatal issue at this point.

                                string errstr = string.Format(CultureInfo.InvariantCulture,
                                                       "Unable to parse ParentID for Work Item {0} based on the ParentShortName value of {1}.",
                                                       pWorkItemId,
                                                       ParentShortName);

                                MptEventLogger.WriteError(errstr);

                            }

                        }
                        else // delimiter was not found
                        {
                            // We don't treat failure to find a proper ParentID as a fatal issue at this point.

                            string errstr = string.Format(CultureInfo.InvariantCulture,
                                                   "Unable to parse ParentID for Work Item {0} based on the ParentShortName value of {1}.  The : delimiter between ID and title is missing.",
                                                   pWorkItemId,
                                                   ParentShortName );

                            MptEventLogger.WriteError(errstr);

                        }


                StartDate       = pStartDate;
                FinishDate      = pFinishDate;

                CompletedWork   = pCompletedWork;
                RemainingWork   = pRemainingwork;

            }   // constructor

        }   // class WorkItemRollupSummary
 




        /// <summary>
        /// This class is reponsible for the MPT parent/child rollup service.  The rollup service rolls up
        /// completed and remaining work as well as earliest start and latest finish dates from tasks to 
        /// deliverables and from deliverables to features.  
        /// </summary>
        public WorkItemRollup(IMptConfiguration mptConfiguration)
        {
            this.mptConfiguration       = mptConfiguration;


            // Make sure there is at least one non-null team server specified -- otherwise we are 
            // completely wasting our time -- the daemon will do absolutely NOTHING because it will
            // never be able to log into even a single server.
                int NonBlankorNullServerCount = 0;
                foreach (TeamServerConfiguration Server in mptConfiguration.TeamServers)
                {
                    if (string.IsNullOrEmpty(Server.ServerName) == false)
                        ++NonBlankorNullServerCount;
                }

                if (NonBlankorNullServerCount == 0)
                {
                   
                    MptException ex = new MptException("There aren't any TFS servers defined in the configuration file.  Update the config file and try again.");
                    MptEventLogger.WriteError(ex.ToString());
                    throw ex;
                }

        }





        /// <summary>
        /// Full rollup run for every team project being serviced by this instance (as controlled in configuration).
        /// A Full process run is differentiated from an Incremental run in that a full run recurses the entire
        /// tree for each project without _any_ regard for any highwatermark. 
        /// </summary>
        /// 
        /// <returns>
        /// Returns integer count of the number of fatal errors encountered and logged.  
        /// </returns>
        /// 
        /// <remarks>
        /// The method loops through each server sequentially on a single thread.  Any fatal error within a 
        /// particular server only stops processing for that particular server.  Such errors are handled, logged
        /// and then processing continues with the next server.
        /// 
        /// Within any particular server, the service dynamically determines which of the the projects identified
        /// in the configuration file have root rollup items on the current server.  It then sequentially loops
        /// through each of those projects.  For each project it sets up a root loop of all features in that project
        /// and then recurses the tree to perform the actual rollup.  If the system cannot access a particular team
        /// project then that problem is logged and work continues for other projects on the same server.
        /// If any problem is encountered under a root feature in a particular team project, then that error is 
        /// handled and logged and then the system continues its attempt to process other features in the same
        /// team project.   
        /// 
        /// In this way, the system attempts to make as much forward progress on as many server/project/features
        /// as possible such that as much of the system is properly rolled up as possible.
        /// 
        /// NOTE:  If ANY error occurs on a server then a new highwatermark cannot be reliably set because by 
        /// definition something would be left behind (at least in an incremental run context).
        /// </remarks>
        [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Design", "CA1031:DoNotCatchGeneralExceptionTypes")]
        public int FullProcess()
        {
            int NbrOfErrorsEncountered = 0;

            IEnumerable<TeamServerConfiguration> serverConfigurations = mptConfiguration.TeamServers;
            foreach (TeamServerConfiguration Server in serverConfigurations)
            {

                try
                {
                    NbrOfErrorsEncountered += FullProcess_SpecificServer(Server);
                }
                catch (Exception e)
                {
                    StringBuilder sb = new StringBuilder();
                    sb.AppendFormat("Fatal error while performing full rebuild for server {0}: {1}",
                                        Server.ServerName, e);

                    MptEventLogger.WriteError(sb.ToString());

                    ++NbrOfErrorsEncountered;
                }

            }

            // update the config file with the lastest config changes
            mptConfiguration.TeamServers = serverConfigurations;

            return (NbrOfErrorsEncountered);

        }   // FullProcess()

        private int FullProcess_SpecificServer(TeamServerConfiguration pServer )
        {
            List<WorkItemRollupSummary> RootFeatureSummaryList;
            DateTime                    FeatureList_AsOfUTC_ForHighwaterMark;


            TeamFoundationServer tfsServer = TeamFoundationServerFactory.GetServer(pServer.ServerName);
            WorkItemStore tfsWorkItemStore = (WorkItemStore)tfsServer.GetService(typeof(WorkItemStore));


            TFS_GetAllFeatures(tfsWorkItemStore,
                               mptConfiguration.TeamProjects,
                                    out RootFeatureSummaryList,
                                    out FeatureList_AsOfUTC_ForHighwaterMark
                               );


            int NbrOfErrorsEncountered = Process_RootFeatureList(tfsWorkItemStore, RootFeatureSummaryList);


            if (NbrOfErrorsEncountered == 0)
            {
                pServer.RollupLastUpdated = FormatDateForLastRollup(FeatureList_AsOfUTC_ForHighwaterMark);
            }
            // else we clearly encountered some errors and therefore cannot persist the new highwater
            // mark (since that would mean we could miss some updates).
            
            return (NbrOfErrorsEncountered);
        }

        /// <summary>
        /// Rollup work is performed only on feature trees (i.e. features->deliverables->tasks) in any team project
        /// being serviced by this instance (as controlled in configuration) that have CHANGED since the last known
        /// highwatermark date/time.  If a highwatermark is unavailable, then a FullRollup run is automatically 
        /// initiated in order to establish a new highwatermark - and thereafter only Incremental Processing need occur. 
        /// </summary>
        /// 
        /// <returns>
        /// Returns integer count of the number of fatal errors encountered and logged.  
        /// </returns>
        /// 
        /// <remarks>
        /// The method loops through each server sequentially on a single thread.  Any fatal error within a 
        /// particular server only stops processing for that particular server.  Such errors are handled, logged
        /// and then processing continues with the next server.
        /// 
        /// Within any particular server, the service attempts to identify the existing highwatermark.  If one
        /// can't be found then a full process is automatically initiated instead of an incremental process (in order
        /// to establish a new highwatermark for that server).   Assuming we have a highwatermark, then the incremental
        /// process first identifies a list of all Features/Deliverables or Tasks that have changed since that timestamp.
        /// The list of actual changed items is then transformed into a net set of "root" features that are "dirty".
        /// Once we have that list, work can progress in exactly the same manner as it would have on a full process - 
        /// namely loop through distinct projects in the root feature list and for each project loop through just the
        /// root/dirty features in that project and then recurse the tree to perform the actual rollup.
        /// 
        /// If the system cannot access a particular team project then that problem is logged and work continues for 
        /// other so called root/dirty feature projects on the same server. If any problem is encountered under a 
        /// root/dirty feature in a particular team project, then that error is handled and logged and then the 
        /// system continues its attempt to process other features in the same team project.   
        /// 
        /// In this way, the system attempts to make as much forward progress on as many server/project/features
        /// as possible such that as much of the system is properly rolled up as possible.  
        /// 
        /// NOTE:  If ANY error occurs on a server then a new highwatermark cannot be reliably set because by 
        /// definition something would be left behind (at least in an incremental run context).
        /// </remarks>
        [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Design", "CA1031:DoNotCatchGeneralExceptionTypes")]
        public int IncrementalProcess()
        {
            TeamFoundationServer        tfsServer;

            List<WorkItemRollupSummary> RootFeatureSummaryList;
            DateTime                    FeatureList_AsOfUTC_ForHighwaterMark;

            int NbrOfErrorsEncountered = 0;
            IEnumerable<TeamServerConfiguration> serverConfigurations = mptConfiguration.TeamServers;
            foreach (TeamServerConfiguration Server in serverConfigurations)
            {
                try
                {
                    DateTime CurrentServer_ExistingRollupHighwatermark_UTC;
                    bool ConfigParseHighwater = DateTime.TryParse(Server.RollupLastUpdated, CultureInfo.InvariantCulture, DateTimeStyles.AdjustToUniversal , out CurrentServer_ExistingRollupHighwatermark_UTC);

                    if ((ConfigParseHighwater == false))
                    {
                        // The highwatermark for the current server is null.  Therefore it is significantly
                        // more efficient to simply perform a full rebuild for all features on the current
                        // server rather than do an incremental rollup for all changes that have occured 
                        // since the server was brought online.
                            NbrOfErrorsEncountered += FullProcess_SpecificServer( Server );

                    }
                    else
                    {
                        tfsServer = TeamFoundationServerFactory.GetServer(Server.ServerName);
                        WorkItemStore tfsWorkItemStore = (WorkItemStore)tfsServer.GetService(typeof(WorkItemStore));
                        
                        TFS_GetAllFeatures_WithChangesInTree_Since(tfsWorkItemStore,
                                                                    mptConfiguration.TeamProjects,
                                                                    CurrentServer_ExistingRollupHighwatermark_UTC,
                                                                        out RootFeatureSummaryList,
                                                                        out FeatureList_AsOfUTC_ForHighwaterMark
                                                                  );


                        IEnumerable<string> ProjectsActuallyEncountered = RootFeatureSummaryList.Select(p => p.TeamProjectName).Distinct().OrderBy(p => p);
                        foreach (string currTeamProject in ProjectsActuallyEncountered)
                        {
                            try
                            {
                                // The real work of rolling up is done one project at a time and then as you will
                                // see within TFS_Rollup_Features() it progresses one feature at a time.
                                // The thematic tone here is to proceed a) in an ordered & predictable fastion
                                // and b) handle as many errors as possible so even if we run into trouble we
                                // get as much "rollup" work completed as possible. 
                                int NumberOfFeaturesWhichFailedDueToFatalError = TFS_Rollup_Features(tfsWorkItemStore,
                                                                                                     currTeamProject,
                                                                                                     RootFeatureSummaryList);


                                NbrOfErrorsEncountered += NumberOfFeaturesWhichFailedDueToFatalError;
                            }
                            catch (Exception e)
                            {
                                StringBuilder sb = new StringBuilder();
                                sb.AppendFormat("Fatal error while performing full rebuild for server {0} project {1}: {2}",
                                                    Server.ServerName,
                                                    currTeamProject,
                                                    e);

                                MptEventLogger.WriteError(sb.ToString());

                                ++NbrOfErrorsEncountered;
                            }

                        } // TeamProject loop


                        if (NbrOfErrorsEncountered == 0)
                        {
                            Server.RollupLastUpdated = FormatDateForLastRollup(FeatureList_AsOfUTC_ForHighwaterMark);
                                
                        }
                        // else we clearly encountered some errors and therefore cannot persist the new highwater
                        // mark (since that would mean we could miss some updates).

                    }
                }
                catch (Exception e)
                {
                    StringBuilder sb = new StringBuilder();
                    sb.AppendFormat("Fatal error while performing incremental rebuild for server {0}: {1}",
                                        Server.ServerName, e);

                    MptEventLogger.WriteError(sb.ToString());

                    ++NbrOfErrorsEncountered;
                }

            }   // Outer Server loop

            // update the config file with latest rollup date
            mptConfiguration.TeamServers = serverConfigurations;

            return (NbrOfErrorsEncountered);
        }

        private static int Process_RootFeatureList(WorkItemStore pWorkItemStore, 
            IEnumerable<WorkItemRollupSummary> pRootFeatureSummaryList)
        {
            int NbrOfErrorsEncountered = 0;
            
            IEnumerable<string> ProjectsActuallyEncountered = pRootFeatureSummaryList.Select(p => p.TeamProjectName).Distinct().OrderBy(p => p);
            foreach (string currTeamProject in ProjectsActuallyEncountered)
            {
                try
                {
                    // The real work of rolling up is done one project at a time and then as you will
                    // see within TFS_Rollup_Features() it progresses one feature at a time.
                    // The thematic tone here is to proceed a) in an ordered & predictable fastion
                    // and b) handle as many errors as possible so even if we run into trouble we
                    // get as much "rollup" work completed as possible. 
                    int NumberOfFeaturesWhichFailedDueToFatalError = TFS_Rollup_Features(pWorkItemStore, currTeamProject,
                                                                                         pRootFeatureSummaryList);


                    NbrOfErrorsEncountered += NumberOfFeaturesWhichFailedDueToFatalError;
                }
                catch (Exception e)
                {
                    StringBuilder sb = new StringBuilder();
                    sb.AppendFormat("Fatal error while performing full rebuild for server {0} project {1}: {2}",
                                        pWorkItemStore.TeamFoundationServer.Name,
                                        currTeamProject,
                                        e);

                    MptEventLogger.WriteError(sb.ToString());

                    ++NbrOfErrorsEncountered;
                }

            } // TeamProject loop



            return (NbrOfErrorsEncountered);
        }

        /// <summary>
        /// This function returns a query which will return the key fields related to rolling
        /// up work for the relevant projects, WITCategories and TimePeriod.  If no team projects 
        /// are specified then the net effect is the query will return all work items of all the
        /// appropriate WITCategories from all projects on the server.  If the HighWaterMark that
        /// is passed in is effectively null then it is ignored.
        /// </summary>
        private static string TFS_Build_Query( IEnumerable<string> pTeamProjects,
                                        IEnumerable<string> pWITCategories,
                                        string              pParentShortName_IgnoreIfNull,
                                        DateTime            pHighWaterMark_UTC_IgnoreIfNull
                                       )
        {
            string ActualParentName;               // ParentShortName with special chars essentially escaped

            string retval_Query = @"
                SELECT
                    [System.TeamProject],
                    [Microsoft.VSTS.Common.WITCategory],
                    [System.Id],
                    [System.Title],
                    [Microsoft.VSTS.Common.ParentShortName],
                    [Microsoft.VSTS.Scheduling.StartDate],
                    [Microsoft.VSTS.Scheduling.FinishDate],
                    [Microsoft.VSTS.Scheduling.CompletedWork],
                    [Microsoft.VSTS.Scheduling.RemainingWork]
                FROM 
                    WorkItems
                WHERE
                    [Microsoft.VSTS.Common.ParentShortName] <> '' AND
                  (";


            // WITCatetory
            string WhereClause_WITCategories = TFS_Build_Query_BuildDelimitedList((ICollection<string>)pWITCategories);
            if (string.IsNullOrEmpty( WhereClause_WITCategories) == false)
                retval_Query += string.Format(CultureInfo.InvariantCulture,
                                               "[Microsoft.VSTS.Common.WITCategory] IN ({0})",
                                                WhereClause_WITCategories);
            else
            {
                // Unlike TeamProjects (which may be null/blank in certain cases where we are 
                // servicing all team projects on an entire server -- WITCategories should
                // absolutely never be blank.  Sometimes it may only have one value (as in the case
                // where we want to get ALL features for relevant team projects) and in other
                // cases it will have multiple values (as in the case where we want all Features,
                // Deliverables or Tasks that have changed since a particular time OR in the case
                // where we want all deliverables for a particular parent.
                        throw new ArgumentException("Invalid value for pWITCategories - not expecting null");
            }


            // NOTE: All of the following Where clause fragements assume that WITCategory went first
            // since they all assume they need to prefix themselves with an AND clause.


            // ParentShortName
            // We completely ignore parent short name unless it has a non-null value.  The reason
            // for this is that there are a couple scenarios where this function is called to get
            // a broad swath of work items (e.g. ALL features or ALL features/deliverables/tasks that
            // have changed since TimeX).
            if (string.IsNullOrEmpty(pParentShortName_IgnoreIfNull) == false)
            {
                // TFS will blow up with 'somevalue's' due to the middle apostrophe
                // WIT queries expect embedded apostrophe values to be quasi-escaped
                // by replacing ' with '' (so in the above example we would expect to 
                // see 'somevalue''s'
                ActualParentName = pParentShortName_IgnoreIfNull.Replace("'", "''");

                retval_Query += " AND ";
                retval_Query += "[Microsoft.VSTS.Common.ParentShortName] = '" + ActualParentName + "'";

            }


            // TeamProject
            string WhereClause_ProjectList = TFS_Build_Query_BuildDelimitedList((ICollection<string>)pTeamProjects);
            if (string.IsNullOrEmpty(WhereClause_ProjectList) == false)
            {
                retval_Query += " AND ";
                retval_Query +=  string.Format(CultureInfo.InvariantCulture, 
                                               "[System.TeamProject] IN ({0})",
                                                WhereClause_ProjectList);
            }


            // ChangedDate -- ignore unless non-null
            if (pHighWaterMark_UTC_IgnoreIfNull != new DateTime())
            {
                // Minor adjustment specfically to try to proactively edge case misses on time query.  For example assume
                // some sort of edge case where our last highwatermark, was at t0 but then virtually simultaneously there
                // was an insert on the server at t0-1millisecond or something like that.  We just don't want
                // a little edge case like that to cause us to miss a potentially relevent update.

                DateTime Adjusted_HighWaterMark_UTC = pHighWaterMark_UTC_IgnoreIfNull.AddMinutes(-5);
              

                // NOTE:  once a DateTime value (which includes time) is added to the
                // where clause -- the caller must use a different API to execute the query
                // and furthermore must explicity the server of its intent to use the
                // as of time. 
                retval_Query += " AND ";
                retval_Query +=  string.Format(CultureInfo.InvariantCulture,
                                               "[System.ChangedDate] >= '{0:yyyy'-'MM'-'dd'T'HH':'mm':'ss'Z'}'",
                                                Adjusted_HighWaterMark_UTC);
            }
            // else date is essentially null and hence it is assumed that the caller wants
            // us to ignore it (by not tacking it into the query string).  
        
            retval_Query += " )";
            
            retval_Query += " ORDER BY [System.TeamProject], [System.Id]";
            
            return (retval_Query);
        }

        /// <summary>
        /// Takes an arbitrary collection of strings and builds a delimited list.  A single
        /// quote character is also added as both a prefix and suffix to each individual string.
        /// The resulting return value looks something like this:  'string1', 'string2'
        /// </summary>
        private static string TFS_Build_Query_BuildDelimitedList(ICollection<string> pStrings)
        {
            string retval;
            
            if (pStrings.Count > 0)
            {
                // Concatenate all of the projects into a comma delimited list.
                StringBuilder sb = new StringBuilder();
                foreach (string s in pStrings)
                {
                    if (sb.Length > 0)
                        sb.Append(", ");

                    sb.Append(string.Format(CultureInfo.InvariantCulture, " '{0}'", s));
                }

                retval = sb.ToString();
            }
            else
                retval = string.Empty;
            
            return (retval);

        }

        /// <summary>
        /// Parses specific field values from an arbitrary MPT work item and returns a WorkItemRollupSummary
        /// class.   Of course only Feature, Deliverable and Task WITCategories have the fields that 
        /// this method is expecting to parse out.
        /// </summary>
        private static WorkItemRollupSummary TFS_ConvertWorkItemToRollupSummary(WorkItem pWorkItem)
        {
            WorkItemRollupSummary Summary = new WorkItemRollupSummary(pWorkItem.Project.Name,
                                                 pWorkItem.Fields["Microsoft.VSTS.Common.WITCategory"].Value.ToString(),
                                                 pWorkItem.Id,
                                                 pWorkItem.Title,
                                                 pWorkItem.Fields["Microsoft.VSTS.Common.ParentShortName"].Value.ToString(),
                                                 Convert.ToDateTime(pWorkItem.Fields["Microsoft.VSTS.Scheduling.StartDate"].Value, CultureInfo.InvariantCulture),
                                                 Convert.ToDateTime(pWorkItem.Fields["Microsoft.VSTS.Scheduling.FinishDate"].Value, CultureInfo.InvariantCulture),
                                                 Convert.ToDouble(pWorkItem.Fields["Microsoft.VSTS.Scheduling.CompletedWork"].Value, CultureInfo.InvariantCulture),
                                                 Convert.ToDouble(pWorkItem.Fields["Microsoft.VSTS.Scheduling.RemainingWork"].Value, CultureInfo.InvariantCulture)
                                             );

            return (Summary);
        }

        /// <summary>
        /// Get ALL features in ALL relevant team projects and transform them into a list of 
        /// WorkItemSummary values.  The method also returns a new highwater mark value 
        /// in UTC timeformat.
        /// </summary>
        private static void TFS_GetAllFeatures(WorkItemStore                   pWorkItemStore,
                                        IEnumerable<string>             pTeamProjects,
                                        out List<WorkItemRollupSummary> retval_RootFeatureList,
                                        out DateTime                    retval_AsOfUTC
                                       )
        {
            List<string>            WITCategoryForJustFeatures;
            string                  QueryStr;
            WorkItemCollection      Features;


            WITCategoryForJustFeatures = new List<string>();
            WITCategoryForJustFeatures.Add("Feature");

            QueryStr = TFS_Build_Query(  pTeamProjects,
                                         WITCategoryForJustFeatures,
                                         null,                      // null ParentShortName because we want ALL features -- not just ones with a particular parent.
                                         new DateTime()             // null date will force ignore of highwatermark -- to get all relevant features whether they have changed or not
                                       ); 


            Features = pWorkItemStore.Query(QueryStr);


            retval_RootFeatureList = new List<WorkItemRollupSummary>();
            foreach (WorkItem Feature in Features)
            {
                WorkItemRollupSummary FeatureSummary = TFS_ConvertWorkItemToRollupSummary(Feature);
                retval_RootFeatureList.Add(FeatureSummary);
            }

            retval_AsOfUTC = Features.AsOfUTC;

        }





        private static void TFS_GetAllFeatures_WithChangesInTree_Since(
                                        WorkItemStore                   pWorkItemStore,
                                        IEnumerable<string>             pTeamProjects,
                                        DateTime                        HighwaterMark_AsOfUTC,
                                        out List<WorkItemRollupSummary> retval_RootFeatureList,
                                        out DateTime                    retval_AsOfUTC
                                       )
        {
            List<WorkItemRollupSummary> Summaries_ExpandedTreeList;        /* Actual changed work items may contain just
                                                                            * tree fragments such as just a task or just
                                                                            * a deliverable etc.  In order to arrive at 
                                                                            * a net list of ROOT feature trees that are
                                                                            * dirty, we need to take what we actually got
                                                                            * and explode the tree up through features.
                                                                            * For example, say only one task has changed.
                                                                            * In that case, we need to take that task and
                                                                            * then walk the tree backward (i.e. upward)
                                                                            * until we find the root feature that is dirty
                                                                            * (i.e. find the parent deliverable and then 
                                                                            * find the parent of the parent deliverable -
                                                                            * which will be the feature).  
                                                                            */


            List<string> WITCategoriesForFeatureTrees = new List<string> {"Feature", "Deliverable", "Task"};
            
            List<WorkItemRollupSummary> Tasks_MissingParentSummaries;
            List<WorkItemRollupSummary> Tasks_ItemsWhoseParentCouldNotBeLoaded;
            List<WorkItemRollupSummary> Deliverables_MissingParentSummaries;
            List<WorkItemRollupSummary> Deliverables_ItemsWhoseParentCouldNotBeLoaded;
            
            string QueryStr = TFS_Build_Query(  pTeamProjects,
                                                WITCategoriesForFeatureTrees,
                                                null,                      // null ParentShortName because we want ALL items in the trees -- not just ones with a particular parent item.
                                                HighwaterMark_AsOfUTC      // we are specifically looking only for work items that have changed since last highwatermark
                );

            Query wiql_Query = new Query(pWorkItemStore, QueryStr, null, false);
                                                                            // critical -- TFS mandates it for queries
                                                                            // that include a time portion of a date
                                                                            // string.

            ICancelableAsyncResult CancelableAsyncResult = wiql_Query.BeginQuery();
            WorkItemCollection ActualChangedWorkItems = wiql_Query.EndQuery(CancelableAsyncResult);


            Summaries_ExpandedTreeList = new List<WorkItemRollupSummary>();
            foreach (WorkItem ChangedItem in ActualChangedWorkItems)
            {
                WorkItemRollupSummary ChangedItemSummary = TFS_ConvertWorkItemToRollupSummary(ChangedItem);
                Summaries_ExpandedTreeList.Add(ChangedItemSummary);
            }



            TFS_GetMissingParents(pWorkItemStore,
                                   Summaries_ExpandedTreeList,
                                   "Task",
                                            out Tasks_MissingParentSummaries,
                                            out Tasks_ItemsWhoseParentCouldNotBeLoaded
                                       );
            Summaries_ExpandedTreeList.AddRange(Tasks_MissingParentSummaries);


            TFS_GetMissingParents(pWorkItemStore,
                                   Summaries_ExpandedTreeList,
                                   "Deliverable",
                                            out Deliverables_MissingParentSummaries,
                                            out Deliverables_ItemsWhoseParentCouldNotBeLoaded
                                       );
            Summaries_ExpandedTreeList.AddRange(Deliverables_MissingParentSummaries);


            //TODO --  need to decide what to do with items whose parents we could not find.

            retval_RootFeatureList = new List<WorkItemRollupSummary>();
            var Query = from SummaryItem in Summaries_ExpandedTreeList
                        where SummaryItem.WITCategory.Equals("Feature", StringComparison.OrdinalIgnoreCase)
                        orderby SummaryItem.WorkItemId
                        select SummaryItem;
            foreach (WorkItemRollupSummary currSummaryItem in Query)
            {
                retval_RootFeatureList.Add(currSummaryItem);
            }

            retval_AsOfUTC = ActualChangedWorkItems.AsOfUTC;

        }

        private static void TFS_GetMissingParents(
                                        WorkItemStore                   pWorkItemStore,
                                        List<WorkItemRollupSummary>     pSummaries_ExpandedTreeList,
                                        string                          pWITCategory,
                                            out List<WorkItemRollupSummary> retval_MissingParents,
                                            out List<WorkItemRollupSummary> retval_ItemsWhoseParentCouldNotBeFound
                                       )
        {
            if (
                    (string.Compare(pWITCategory, "Task",        StringComparison.OrdinalIgnoreCase) == 0) ||
                    (string.Compare(pWITCategory, "Deliverable", StringComparison.OrdinalIgnoreCase) == 0)
                )
            {
                retval_MissingParents                  = new List<WorkItemRollupSummary>();
                retval_ItemsWhoseParentCouldNotBeFound = new List<WorkItemRollupSummary>();


                var Query = from SummaryItem in pSummaries_ExpandedTreeList
                            where SummaryItem.WITCategory.Equals(pWITCategory, StringComparison.OrdinalIgnoreCase)
                            orderby SummaryItem.WorkItemId
                            select SummaryItem;

                foreach (WorkItemRollupSummary currSummaryItem in Query)
                {
                    bool ParentItemAlreadyExistsInExpandedTreeList = pSummaries_ExpandedTreeList.Exists(
                        item => item.WorkItemId == currSummaryItem.ParentID
                        );

                    if (ParentItemAlreadyExistsInExpandedTreeList == false)
                    {
                        try
                        {
                            WorkItem ParentWorkItem = pWorkItemStore.GetWorkItem(currSummaryItem.ParentID);

                            WorkItemRollupSummary NewParentSummaryItem = TFS_ConvertWorkItemToRollupSummary(ParentWorkItem);

                            retval_MissingParents.Add(NewParentSummaryItem);

                        }
                        catch (Exception)
                        {
                            //TODO:  figure out how seriously to treat this
                            retval_ItemsWhoseParentCouldNotBeFound.Add(currSummaryItem);
                        }
                    }
                    // else we already have the parent item in the expanded tree list - so no work to do.
                }
            }
            else
            {
                string errstr = string.Format(CultureInfo.InvariantCulture,
                                       "Invalid pWITCategory value {0}.  Only Deliverable or Task is expected.",
                                       pWITCategory);

                throw new ArgumentException(errstr);
            }


        }

        /// <summary>
        /// This small helper function takes a the title of a particular work item and concatenates
        /// its ID to form a ParentShortName.  The resulting parent short name can be used in queries
        /// to find so called children of the work item.
        /// </summary>
        private static string TFS_ParentShortName(int pParentID, string pParentShortName)
        {
            string retval = string.Concat(pParentID.ToString(CultureInfo.InvariantCulture), ": ", pParentShortName);

            return (retval);
        }


        /// <summary>
        /// Loop through deliverable children of ParentFeature and for each deliverable rollup child tasks.
        /// Return aggregate results of dates/work found under ParentFeature.
        /// </summary>
        private static bool TFS_Rollup_Deliverables(WorkItemStore         pWorkItemStore,
                                             WorkItemRollupSummary ParentFeature,
                                                                        out DateTime retval_EariestNonNullStartDate,
                                                                        out DateTime retval_LatestNonNullFinishDate,
                                                                        out double retval_CompletedWork,
                                                                        out double retval_RemainingWork
                                            )
        {
            bool retval_Found_Work_UNDER_CurrParentWorkItem;
                                                            /* If this value is True then the system will
                                                             * take over the maintenance of the work totals
                                                             * on the parent. If False then the work totals are 
                                                             * managed by humans rather than the system.    
                                                             */

            List<string>        WITCategoryForJustDeliverables;

            List<string>        Parent_Project;
            int                 Feature_ID;
            string              Feature_UniqueName;
            string              QueryStr;
            WorkItemCollection  Deliverables;

            DateTime            curr_StartDate;
            DateTime            curr_FinishDate;
            double              curr_CompletedWork;
            double              curr_RemainingWork;
            bool                Found_Work_Under_CurrDeliverable;

            DateTime            Children_EariestNonNullStartDate;
            DateTime            Children_LatestNonNullFinishDate;
            double              Children_CompletedWork;
            double              Children_RemainingWork;



            //INITIALIZE RETURN VALUES
            retval_Found_Work_UNDER_CurrParentWorkItem = false;
            retval_EariestNonNullStartDate             = new DateTime();
            retval_LatestNonNullFinishDate             = new DateTime();
            retval_CompletedWork                       = 0.0;
            retval_RemainingWork                       = 0.0;


            // Find all the child deliverables of the current parent feature
                // The TFS_Build_Query() api is used in several different contexts and as such has a method
                // signature to handle that flexibility.  The side effect of that fact is that we have a 
                // little extra grunt work to do in order to get the WITCategory and TeamProject name
                // into the proper form (since TFS_Build_Query expects something with an IEnumerable interface).
                    WITCategoryForJustDeliverables = new List<string>();
                    WITCategoryForJustDeliverables.Add("Deliverable");

                    Parent_Project = new List<string>();
                    Parent_Project.Add(ParentFeature.TeamProjectName);

            Feature_ID         = ParentFeature.WorkItemId;
            Feature_UniqueName = TFS_ParentShortName(Feature_ID, ParentFeature.Title);

            QueryStr = TFS_Build_Query(Parent_Project,
                                         WITCategoryForJustDeliverables,
                                         Feature_UniqueName,
                                         new DateTime()    // null date will force ignore of highwatermark -- to get ALL relevant children whether they have changed or not
                                       );

            Deliverables = pWorkItemStore.Query(QueryStr);


            // If we found work under the parent, we set the return value flag so that the caller will know
            // to go ahead an update the feature (provided of course that the resulting totals are materially 
            // different from the current parent feature values).
            if (Deliverables.Count > 0)
                retval_Found_Work_UNDER_CurrParentWorkItem = true;


            foreach (WorkItem Deliverable in Deliverables)
            {
                // Initialize current summary values
                curr_StartDate = Convert.ToDateTime(Deliverable.Fields["Microsoft.VSTS.Scheduling.StartDate"].Value, CultureInfo.InvariantCulture);
                curr_FinishDate = Convert.ToDateTime(Deliverable.Fields["Microsoft.VSTS.Scheduling.FinishDate"].Value, CultureInfo.InvariantCulture);
                curr_CompletedWork = Convert.ToDouble(Deliverable.Fields["Microsoft.VSTS.Scheduling.CompletedWork"].Value, CultureInfo.InvariantCulture);
                curr_RemainingWork = Convert.ToDouble(  Deliverable.Fields["Microsoft.VSTS.Scheduling.RemainingWork"].Value, CultureInfo.InvariantCulture);


                // Initialize children summary values to essentially null values
                Children_EariestNonNullStartDate = new DateTime();
                Children_LatestNonNullFinishDate = new DateTime();
                Children_CompletedWork           = 0.0;
                Children_RemainingWork           = 0.0;


                Found_Work_Under_CurrDeliverable = TFS_Rollup_Tasks(pWorkItemStore,
                                                                    Deliverable,
                                                                     out Children_EariestNonNullStartDate,
                                                                     out Children_LatestNonNullFinishDate,
                                                                     out Children_CompletedWork,
                                                                     out Children_RemainingWork
                                                                    );


                if (
                         ((Math.Abs((Children_CompletedWork - curr_CompletedWork)) >= 0.1) ||
                          (Math.Abs((Children_RemainingWork - curr_RemainingWork)) >= 0.1))
                    ||
                         ((curr_StartDate  != Children_EariestNonNullStartDate) ||
                          (curr_FinishDate != Children_LatestNonNullFinishDate))

                    )
                {
                    // Since there is a material difference in either the work totals or rollup dates, we 
                    // will at least consider updating the deliverable.
                    //
                    // A check is made for whether we actually found child work because if none was actually 
                    // found then there will be a material difference (e.g. null != current total) but the 
                    // null is essentially moot because there were no children -- and hence we don't want
                    // default (aka null) values to overwrite totals on the current work item which are 
                    // apparently being managed by humans.

                    if (Found_Work_Under_CurrDeliverable)
                    {

                        Deliverable.Open();

                        // We only rollup non-null dates from tasks.  As such, if we get a null date
                        // back from tasks, then it essentially means that there was no corresponding
                        // date value (either start or finish) on any of the child tasks.  In other words,
                        // if we get a null earliest start date back -- then a) no tasks had a start date
                        // and therefore b) we don't update the deliverable start date with a null.

                        if (Children_EariestNonNullStartDate != new DateTime())
                            Deliverable.Fields["Microsoft.VSTS.Scheduling.StartDate"].Value = Children_EariestNonNullStartDate;

                        if (Children_LatestNonNullFinishDate != new DateTime())
                            Deliverable.Fields["Microsoft.VSTS.Scheduling.FinishDate"].Value = Children_LatestNonNullFinishDate;


                        Deliverable.Fields["Microsoft.VSTS.Scheduling.CompletedWork"].Value = Children_CompletedWork;
                        Deliverable.Fields["Microsoft.VSTS.Scheduling.RemainingWork"].Value = Children_RemainingWork;

                        Deliverable.Save();
                    }
                    else
                    {
                        // The current deliverable had zero child 'links' -- in which case the design request 
                        // is to leave the totals on the deliverable alone.
                    }


                }
                // else there isn't enough of a change to warrant creating a useless revision on the deliverable.



                // UPDATE RETURN ROLLUP VALUES

                // Since current deliverable MAY have just changed based on rollups from 
                // tasks -- we can simply reset our current values to accurately reflect any update
                // (if any) that was just made to the deliverable.
                curr_StartDate = Convert.ToDateTime(Deliverable.Fields["Microsoft.VSTS.Scheduling.StartDate"].Value, CultureInfo.InvariantCulture);
                curr_FinishDate = Convert.ToDateTime(Deliverable.Fields["Microsoft.VSTS.Scheduling.FinishDate"].Value, CultureInfo.InvariantCulture);
                curr_CompletedWork = Convert.ToDouble(Deliverable.Fields["Microsoft.VSTS.Scheduling.CompletedWork"].Value, CultureInfo.InvariantCulture);
                curr_RemainingWork = Convert.ToDouble(Deliverable.Fields["Microsoft.VSTS.Scheduling.RemainingWork"].Value, CultureInfo.InvariantCulture);

                // Update return dates only if necessary (because of the earliest/latest nature of that rollup
                // as well as the fact that we ONLY rollup earliest/latest NON-NULL dates
                if (curr_StartDate != new DateTime())
                {
                    if ( (curr_StartDate < retval_EariestNonNullStartDate) ||
                         (retval_EariestNonNullStartDate == new DateTime())  )
                        retval_EariestNonNullStartDate = curr_StartDate;
                }

                if (curr_FinishDate != new DateTime())
                {

                    if (curr_FinishDate > retval_LatestNonNullFinishDate)
                        retval_LatestNonNullFinishDate = curr_FinishDate;
                }

                retval_CompletedWork += curr_CompletedWork;
                retval_RemainingWork += curr_RemainingWork;


            }   // foreach Deliverable


            return (retval_Found_Work_UNDER_CurrParentWorkItem);

        }

        /// <summary>
        /// For each Feature in the root list passed into this funciton, fully recurse the tree looking for
        /// child work and then rollup value as appropriate.  Work proceeds one feature at a time and if 
        /// any error occurs on a particular feature then that error is logged and handled before work 
        /// continues on with other features.  The idea here is to attempt to bring as many of the features 
        /// up-to-date as possible.
        /// </summary>
        private static int TFS_Rollup_Features(WorkItemStore               pWorkItemStore,
                                        string                      pTeamProject, 
                                        IEnumerable<WorkItemRollupSummary> pRootFeatureSummaryList)
        {
            bool        Found_Work_Under_CurrFeature;

            DateTime    Children_EariestNonNullStartDate;
            DateTime    Children_LatestNonNullFinishDate;
            double      Children_CompletedWork;
            double      Children_RemainingWork;

            int         retval_NbrOfFeaturesWhichFailedDueToFatalError;



            retval_NbrOfFeaturesWhichFailedDueToFatalError = 0;


            var FeatureQuery = from    Feature in pRootFeatureSummaryList
                               where   Feature.TeamProjectName.Equals(pTeamProject, StringComparison.OrdinalIgnoreCase)
                               orderby Feature.WorkItemId
                               select  Feature;

            foreach (WorkItemRollupSummary currFeatureSummary in FeatureQuery)
            {

                try
                {
                    // Initialize children summary values to essentially null values
                    Children_EariestNonNullStartDate = new DateTime();
                    Children_LatestNonNullFinishDate = new DateTime();
                    Children_CompletedWork           = 0.0;
                    Children_RemainingWork           = 0.0;


                    Found_Work_Under_CurrFeature = TFS_Rollup_Deliverables( pWorkItemStore,
                                                                            currFeatureSummary,
                                                                            out Children_EariestNonNullStartDate,
                                                                            out Children_LatestNonNullFinishDate,
                                                                            out Children_CompletedWork,
                                                                            out Children_RemainingWork
                                                                          );

                    if (
                            ( (Math.Abs((Children_CompletedWork - currFeatureSummary.CompletedWork)) >= 0.1) ||
                              (Math.Abs((Children_RemainingWork - currFeatureSummary.RemainingWork)) >= 0.1)   )
                        ||
                            ( (currFeatureSummary.StartDate  != Children_EariestNonNullStartDate) ||
                              (currFeatureSummary.FinishDate != Children_LatestNonNullFinishDate)    )

                        )
                    {
                        // Since there is a material difference in the work totals, we will at least consider updating
                        // the feature totals.  A check is made for whether we actually found child work because if 
                        // none was actually found then there will be a material difference (e.g. null != current total)
                        // but the null is essentially moot because there were no children -- and hence we don't want
                        // default (aka null) values to overwrite totals on the current work item which are apparently
                        // being managed by humans.

                        if (Found_Work_Under_CurrFeature)
                        {
                            WorkItem Feature = pWorkItemStore.GetWorkItem(currFeatureSummary.WorkItemId);


                            // We only rollup non-null dates from deliverables.  As such, if we get a null date
                            // back from deliverables, then it essentially means that there was no corresponding
                            // date value (either start or finish) on any of the child deliverables.  In other
                            // words, if we get a null earliest start date back -- then a) no deliverables had a
                            // start date and therefore b) we don't update the feature start date with a null.
                                if (Children_EariestNonNullStartDate != new DateTime())
                                    Feature.Fields["Microsoft.VSTS.Scheduling.StartDate"].Value  = Children_EariestNonNullStartDate;

                                if (Children_LatestNonNullFinishDate != new DateTime())
                                    Feature.Fields["Microsoft.VSTS.Scheduling.FinishDate"].Value = Children_LatestNonNullFinishDate;

                            Feature.Fields["Microsoft.VSTS.Scheduling.CompletedWork"].Value = Children_CompletedWork;
                            Feature.Fields["Microsoft.VSTS.Scheduling.RemainingWork"].Value = Children_RemainingWork;

                            Feature.Save();

                        }
                        else
                        {
                            // The current feature had zero child 'links' -- in which case the design request 
                            // is to leave the totals on the feature alone.
                        }


                    }
                    // else there isn't enough of a change to warrant creating a useless revision on the feature.

                }
                catch (Exception e)
                {
                    StringBuilder sb = new StringBuilder();
                    sb.AppendFormat("Fatal error while performing full rebuild for Feature({0}) project {1}: {2}",
                                        currFeatureSummary.WorkItemId,
                                        pTeamProject,
                                        e);

                    MptEventLogger.WriteError(sb.ToString());

                    ++retval_NbrOfFeaturesWhichFailedDueToFatalError;
                }


            } //FeatureQuery loop


            return (retval_NbrOfFeaturesWhichFailedDueToFatalError);

        }   // TFS_Rollup_Features()




        /// <summary>
        /// Rollup work under ParentDeliverable and return results.  Zero updates are ever performed on tasks.
        /// </summary>
        private static bool TFS_Rollup_Tasks(WorkItemStore pWorkItemStore,
                                      WorkItem      ParentDeliverable,
                                            out DateTime retval_EariestNonNullStartDate,
                                            out DateTime retval_LatestNonNullFinishDate,
                                            out double   retval_CompletedWork,
                                            out double   retval_RemainingWork
                                     )
        {
            bool                retval_Found_Work_UNDER_CurrParentWorkItem;
                                                                /* If this value is True then the system will
                                                                 * take over the maintenance of the work totals
                                                                 * on the parent. If False then the work totals are 
                                                                 * managed by humans rather than the system.    
                                                                 */

            List<string>        WITCategoryForJustTasks;

            List<string>        Parent_Project; 
            int                 Deliverable_ID;
            string              Deliverable_UniqueName;
            string              QueryStr;
            WorkItemCollection  Tasks;

            DateTime            curr_StartDate;
            DateTime            curr_FinishDate;
            double              curr_CompletedWork;
            double              curr_RemainingWork;

            //INITIALIZE RETURN VALUES
            retval_Found_Work_UNDER_CurrParentWorkItem = false;
            retval_EariestNonNullStartDate             = new DateTime();
            retval_LatestNonNullFinishDate             = new DateTime();
            retval_CompletedWork                       = 0.0;
            retval_RemainingWork                       = 0.0;


            // Find all the child tasks of the current parent deliverable
                // The TFS_Build_Query() api is used in several different contexts and as such has a method
                // signature to handle that flexibility.  The side effect of that fact is that we have a 
                // little extra grunt work to do in order to get the WITCategory and TeamProject name
                // into the proper form (since TFS_Build_Query expects something with an IEnumerable interface).
                    WITCategoryForJustTasks = new List<string>();
                    WITCategoryForJustTasks.Add("Task");

                    Parent_Project = new List<string>();
                    Parent_Project.Add(ParentDeliverable.Project.Name);

                    Deliverable_ID = Convert.ToInt32(ParentDeliverable.Fields["ID"].Value, CultureInfo.InvariantCulture);
            Deliverable_UniqueName = TFS_ParentShortName(Deliverable_ID, ParentDeliverable.Fields["Title"].Value.ToString());

            QueryStr = TFS_Build_Query(  Parent_Project,
                                         WITCategoryForJustTasks,
                                         Deliverable_UniqueName,
                                         new DateTime()    // null date will force ignore of highwatermark -- to get ALL relevant children whether they have changed or not
                                       );
            Tasks    = pWorkItemStore.Query(QueryStr);


            // If we found work under the parent, we set the return value flag so that the caller will know
            // to go ahead an update the deliverable (provided of course that the resulting totals are materially 
            // different from the current parent deliverable values).
            if (Tasks.Count > 0)
                retval_Found_Work_UNDER_CurrParentWorkItem = true;


            foreach (WorkItem Task in Tasks)
            {

                // Initialize current summary values
                curr_StartDate = Convert.ToDateTime(Task.Fields["Microsoft.VSTS.Scheduling.StartDate"].Value, CultureInfo.InvariantCulture);
                curr_FinishDate = Convert.ToDateTime(Task.Fields["Microsoft.VSTS.Scheduling.FinishDate"].Value, CultureInfo.InvariantCulture);
                curr_CompletedWork = Convert.ToDouble(Task.Fields["Microsoft.VSTS.Scheduling.CompletedWork"].Value, CultureInfo.InvariantCulture);
                curr_RemainingWork = Convert.ToDouble(Task.Fields["Microsoft.VSTS.Scheduling.RemainingWork"].Value, CultureInfo.InvariantCulture);

                // Update return dates only if necessary (because of the earliest/latest nature of that rollup
                // as well as the fact that we ONLY rollup earliest/latest NON-NULL dates
                if (curr_StartDate != new DateTime())
                {
                    if ( (curr_StartDate < retval_EariestNonNullStartDate) ||
                         (retval_EariestNonNullStartDate == new DateTime())   )
                        retval_EariestNonNullStartDate = curr_StartDate;
                }


                if (curr_FinishDate != new DateTime())
                {
                    if (curr_FinishDate > retval_LatestNonNullFinishDate)
                        retval_LatestNonNullFinishDate = curr_FinishDate;
                }

                retval_CompletedWork += curr_CompletedWork;
                retval_RemainingWork += curr_RemainingWork;

            }   // foreach Task


            return (retval_Found_Work_UNDER_CurrParentWorkItem);

        } // TFS_Rollup_Tasks

        private static string FormatDateForLastRollup(DateTime highwaterMark)
        {
            return string.Format(CultureInfo.InvariantCulture,
                                 "{0:yyyy'-'MM'-'dd'T'HH':'mm':'ss'Z'}",
                                 highwaterMark);
        }

    }   // class WorkItemRollup

}   // namespace MptCodeLibrary
